1. 云原生数据湖下对象存储的挑战
在过去的十年,云原生数据湖发展势不可挡。目前基于对象存储底座的云原生数据湖,几乎已经成为了云上数据湖的共识。作为新兴发展的技术,云原生的数据湖有着其巨大的优势:- 资源按需提供,可以提供多种不同价位、不同性能要求的存储类型以及海量的吞吐能力;
- 直接利用云上成熟的生态能力来处理常见的数据治理(监管、风控、归档、加密)及基础计算需求;
- 云供应商会有专业的运维人员维护基础设施,可以极大程度地减少用户的人力运维成本。
随着大量大数据分析、AI 训练等数据驱动的计算业务的不断铺开,我们注意到基于对象存储底座的云原生数据湖也存在一些问题。 例如部分任务在对象存储中运行的性能要弱于直接使用 HDFS 的情况,而这主要是由如下⼏个因素导致:
- 大数据计算场景通常会基于文件系统语义操作数据,而目前的对象存储的元数据几乎全部是平坦 namespace,对于一些文件语义操作的支持并不友好:
- Read dir(List 目录)性能和效率非常低。由于平坦 namespace 的对象存储采用的是文件全路径作为对象名,底层存储通常会基于此对象名的字典序进行顺序存储,因此对于列出目录操作(如 ls a/ )存在需要大量的额外计算和读取放大(比如跳过子目录下的所有文件)的问题;
- Move(Rename)的性能和效率低。由于大数据计算过程非常依赖 Move(Rename)操作,尤其是对于目录的 Move 操作。在对象存储中如果要实现 Move 一个目录 a/ 到 b/ 的操作,在平坦 namespace 的情况下,则需要把 a/ 目录下的所有对象都做一次 Copy+Del 操作,将操作数量放大了 2*N 倍(N 为目录下的对象个数,目录文件数越多则放大越大)。
- Stat 操作需要多次交互操作才能得到结果。在平坦对象存储中,Stat 一个目录a/,部分情况下可能面临高达 3 次 RPC。而 Stat 一个文件 a,在一些情况下也需要 2 次 RPC 才能判断;
对象存储基于 HTTP 协议访问,主流使用的 HTTP/1.1 不支持 pipeline 模式,无法达到更好的业务并发度,从而限制整体请求性能,HTTP/2 和 HTTP/3 在不少场景下并未完全普及。同时,在对象存储中用户的请求需要额外的计算去处理复杂的 ACL 语义,部分程度上影响了对象存储的首字节延迟;数据计算中会有大量的数据流动,而基于存算分离架构云原生数据湖不可避免会出现网络上的额外延迟。HDFS 通常在用户的 VPC 内,能让计算就近访问数据,从而加速计算任务。其中,导致上文第一点的主要原因是目前平坦 namespace 对象存储普遍采用以全路径作为 key 方式进行元数据的存储,因此不具备真正意义上的目录逻辑,所有的目录类操作都会被一一转化为多次对象类操作,需要相当复杂的额外代价去实现目的,从而极大地影响了该类操作的性能。其结果就是一个简单用户操作,比如 Stat 文件或者目录,MV 目录,或者 LS 目录的场景下实际产生高达成千上百倍的额外放大。关于第二个因素,对象存储确实会因此导致更慢的首字节延迟,但是对象存储所具备的丰富 ACL 带来的安全可控特性以及 HTTP 协议带来的兼容易用性实际上是远远是利大于弊的(额外几 ms 的处理耗时),舍弃这些优点显然非常不值得。我们也很容易想到当数据吞吐量(相比起请求数而言)足够高的时候,对整体计算性能的影响依然可控。但是如果当计算任务中对对象存储的请求数量有显著提升的时候,则会放大对象存储首字节慢导致的影响面(比如第一点所述的原因导致了请求数被显著放大,例如对象存储每个请求首字节都会比普通文件系统多几 ms 到十几 ms 的延迟,当单次目录类的操作又因为平层 namespace 对象模型而被成倍放大为更多次的对象存储接口操作后,所消耗的额外耗时将会大大增加),从而拖累整体的计算性能。如果能够将无效的请求数大幅降低,即使对象请求的首字节依然存在延迟,但相比起数据吞吐和计算这些必须的耗时阶段而言,此时的额外耗时占比就会很低,将会使得整个任务的整体耗时处在一个可以接受的范围内。对于第三个因素,由于对象存储可以提供海量的带宽,可以满足任务在计算时所需求的带宽吞吐。如果从整个作业的宏观生命周期上来看,计算任务受到网络传输导致的影响会很有限。但是由于很多场景下,我们在单个节点上的计算任务会采用“读取数据”->“计算数据”的串行模式,这种模式会带来一段计算资源盲等的时间,造成最终计算时间的增加。通过优化计算模型为 pipeline(按照 block 方式将后续的读取与计算并行化),可以有效提升基于存算分离架构下的计算性能,但是这种基于计算逻辑本身的优化非常依赖实际业务的使用方,很难要求所有的业务场景去根据对象存储的特点去调整已有计算模型的实现。这个问题本身是存算分离方案导致的固有问题,好在随着新硬件的使用,SDN 的不断优化、用户侧 cache 的使用,对象存储的性能提升、以及计算业务模型不断的云原生化,这个差距可以预见在未来将被不断缩小。综上分析,如果我们着重把重心放在了第一点即让对象存储去支持目录语义,那么第二个缺点则会因为请求数量的急剧降低而不再影响整体计算性能,并且这项提升几乎不依赖客户业务端代码的改动。而第三点目前只能通过优化网络链路,近客户端 cache 的使用(比如数据湖存储加速 RapidFS)等方式来减少延迟。2. 层级 namespace 对象存储的技术实践
为了尽可能减少因为对象存储而引起的大数据计算性能问题,百度沧海·存储团队提供了基于目录树元数据结构(层级 namespace)的对象存储。该产品与传统对象存储最大的区别在于,其元数据的底层是采用目录树的拓扑结构去存储,因此在处理针对目录相关操作的时候(如 List,Rename,Delete),能够获得相比平坦 namepsace 的对象存储更加理想的性能。同时,我们也期望业务通过基于层级 namespace 的对象存储服务,可以在相当多的计算场景下不再需要为了加速数据计算而引入一些额外的数据迁移和存储(比如迁移到对大数据计算更友好的平台、服务上等),以减少客户潜在的数据复制、流通和管理的成本。接下来我们将全面介绍下百度智能云的层级 namespace 对象存储的技术实现。对象存储 BOS 服务是基于分层架构设计实现的,通过将写入切分为了对象元数据和对象数据两条不同的数据流来分别完成数据写入。对象的元数据层面和数据层面分别通过不同的存储底座实现。利用以上机制,我们可以针对这两类性质不同的数据分别做更加有针对性的优化和实现。层级 namespace 与平坦 namepace 对象存储基于完全相同的数据存储底座和数据入口,共享后端计算、存储、带宽资源。而层级 namespace 的元数据层面则专注于树状层级结构的实现及高性能读写。从上图可以看到,层级 namespace 的数据流中,用户的对象数据会通过数据流传递到底层的基于实时在线 EC 的分布式存储,而元信息则会被路由到基于多副本的高性能分布式集群中。基于分层的数据流,我们可以对于用户的数据做更好的优化,在保证高吞吐、低成本的特性之外,还能提供更好的查询性能。
2.1 层级 namespace 的元数据模型
对象存储 BOS 的层级 namespace 采用如下方式存储元数据:
作为对比,对象存储 BOS 的平坦 namespace 采用的则是如下方式存储元数据:
与平坦 namespace 对象存储结构相比,层级 namespace 采用目录树方式组织对象元数据后,由于记录了明确的层次(父目录与子文件的层级)的关系,这种存储模型能够天然解决上述目录操作涉及到的几个痛点:- ListObjects(Read dir)效率非常高。由于目录下的对象可以用父目录索引(类 inode)直接基于主键前缀检索,因此只需要 1 次 Scan 查询就可以满足列出目录下对象的需求。而平坦 namespace 的对象存储,由于存储方式是全路径,因此则需要对对象数据进行一定的计算处理后才能返回结果(比如有一种做法是跳过子目录下的对象);
- 能够直接高效地实现原子的 Move Dir(Rename Dir)以及 Delete Dir。对于目录的 Rename,只需要将其从父目录的索引下删除,并事务原子地写入到目的目录下即可。而对于删除目录操作,可以复用 Rename 语义,并通过异步删除的方式在后台异步逐个删除目录下的对象即可。
- 在 Stat 语义上可以实现与文件系统完全相同的效果。由于不存在同名的对象和目录(父目录相同的同名文件会被禁止写入,不会同时存在 a/ 目录及 a 对象。作为对比,平坦 namespace 对象存储中,可以同时存在 a/ 目录以及 a 对象),并且树状的存储结构的存储让每个对象都有其对应归属的父节点,故只需要通过一次 HEAD /a/b,就可直接判断出 a/b 是否存在,以及 a/b 究竟是目录还是文件(先 Lookup 到 a/ 目录,再点查 b 的属性信息,可以得到一个唯一的结果)。
2.2 层级 namespace 的数据及接口模型
基于层级 namespace 的对象存储 BOS 通过使用基于目录树的数据格式,能很好地解决传统对象存储在大数据计算场景下的缺陷。通过让层级 namespace 支持递归创建不存在的父目录节点(可以直接创建 /a/b/c 的对象而不需要预先创建 /a,/a/b 等父目录)以及对非空目录的原子删除的支持,我们在接口的实现上更好地兼容了原有平坦 namespace 的使用场景,以及原有接口的定义,实现了用户业务逻辑的零调整即可直接使用层级 namespace。而这些优化在大数据场景下,可以有效减少计算过程中对父目录路径反复的 Lookup,减少在删除目录操作时对子目录额外 Read dir 等的多余操作。同时,BOS 层级 namespace 完全兼容原生对象接口的架构,使得它拥有和传统对象存储一样的优势:- 享受对象存储的超低成本、无限容量、极大的吞吐带宽优势,使用对象存储的丰富生态,无缝集成对象存储的生命周期管理,图像/音视频处理,监控审查等能力;
- 由于数据上云已经成为一个非常普遍的场景,因此一些数据文件可能本身就在对象存储之上,使用层级 namespace 可以让大数据计算直接在对象存储之上进行,无需数据复制,无额外的运维存储和管理成本。
2.3 性能优化和技术实现
虽然通过使用层级元数据架构使得对象存储可以支持对目录高效的操作,但是为了能显著减少大数据计算的整体耗时,我们还需要更高性能的元数据读写能力。不仅如此,日益增长的数据需求也对层级 namespace 的存储规模提出了更高要求,需要保证可以处理海量的对象。同时我们也发现,不少用户的场景虽然有大数据计算的需求,但是在数据导入、业务流程中依然有可能需要通过平坦 namespace 的对象接口来实现对存储资源的访问,对象服务接口的兼容仍然是一个不可抛弃的需求。因此,如何做到与平坦 namespace 对象接口的高度兼容也是我们需要完成的挑战。不同于数据存储通常采用 TCO 更低的存储介质和服务器,层级 namespace 的元数据集群为了追求元数据操作性能最大化,全部采用了具备更高性能的存储介质和计算设备。并且为了满足更高的性能需求,层级 namespace 分别在大数据计算的读写场景下,做了大量的独有优化。2.4 写场景优化
为了支持海量文件的存储,层级 namespace 后端使用一套分布式 K-V 存储去存储所有的目录和文件元信息,并且通过拆分目录和文件信息分别存储,压缩元信息数据,可以做到让文件数突破内存/磁盘容量限制的瓶颈,达到单 Bucket 百亿规模。
数据可靠性是存储服务商的生命线,层级 namespace 的元数据信息存储采用了基于 Raft 框架的多副本方式实现,服务会将用户的所有写入操作转化为完全幂等的 K-V 键值对操作,并通过 Raft 协议保证集群多数副本的一致性,同时可以在必要的时候灵活地添加和迁移副本到不同的后端,从而满足不同的场景下业务对写入的需求。除了借助于新硬件和高性能计算网络设备带来的高性能,我们写入流程的各个方面都做了大量优化。对于用户的每个写入请求,我们使用路径锁来避免一些非预期的并发请求。通过优化读写路径锁的实现,当发现不会有造成冲突的目录类操作的时候,层级 namespace 就会让不同写入请求完全并发执行,从而提升并发能力。通过 Batch Commit 机制把多个任务一起提交到状态机里,并且在确认日志被多数提交的时候,批量执行 Apply Log,再次将多个请求的多个 K-V 操作进行打包从而大幅提升数据写入的能力。在单次写入路径上,我们也做了大量的优化。由于层级 namespace 采用了 Raft 的框架,层级 namespace 的后端存储去掉了额外一次对 WAL 文件写入,而是通过 Raft 状态机日志回放的能力来保证幂等操作的重现,在提高写入性能的同时保证了数据的高可靠。2.5 读场景优化
计算业务往往伴随着大量的数据读取,因此元数据的读性能将会是影响整个计算任务耗时的关键。使用层级 namespace 的元数据架构后,可以减少大量目录相关的元数据操作,但是也有一些关键问题需要解决。层级 namespace 由于采用树状元数据,相比起平坦 namespace,这种存储结构天然引入了一个缺点:每个请求都需要从 Root 根节点依次递归读取路径上所有父目录的属性信息一直到目标节点,导致请求在后端会被放大。举个简单的例子,如果我们想要去读写 /a/b/c/d 节点,那么在实际对目标节点进行操作之前,首先就需要依次访问 /a, /a/b, /a/b/c, /a/b/c/d,从而产生内部读放大的现象。针对这个问题,我们采用维护一个强一致的目录缓存方式来解决,通过内部多版本机制来实现这个缓存实例的过期,确保读写请求的线性一致性。实际上,通过引入目录树缓存,能让绝大多数请求中逐级路径的点查(Look Up)操作都能够命中缓存,从而大幅降低整体延迟,进一步提高并发能力。考虑到大数据计算是一个读远远大于写的场景,并且随着计算节点的弹性扩容,对存储服务的读请求能力要求将会不断变高。基于 Raft 多副本的 BOS 层级 namespace 对于读扩容的场景可以简单地通过增加从节点数量的方式来线性地提升服务处理读请求的能力。当发现用户的读请求数量超过一定阈值之后,就会主动扩容副本数,提升系统的读并发能力。不仅如此,我们对于每个用户的读 /List 请求,通过引入 MVCC 机制达到 Snapshot Read 的隔离级别,因此无论是 Raft 的主副本还是从副本,都可以让读写请求完全并发,从而提升了单个节点的处理能力。由于在大数据计算场景下,在计算作业执行中,经常会出现写后读(List After Write,Read After Write)的场景,并且一旦主从副本之间出现了不一致的场景,就很有可能导致最后计算任务的出错,因此计算天然对强一致性有很高的要求。层级 namespace 中我们通过采用 Leader Lease 的机制来避免脑裂来解决双主问题之外,我们在从节点上也引入了 Read Index 机制,从节点上的读请求将会等到主节点当前已经 commited index 被完全 apply 之后才会真正执行并返回给用户,来保证客户的线性一致性要求,确保在任意元数据节点读到的结果都符合预期。2.6. 对象接口兼容优化
由于层级 namespace 的底层实现上与平坦 namespace 的对象存储有一定区别,导致如果单纯按照文件系统语义去实现,则会对用户原有的业务产生一定影响。举个简单的例子,例如有些用户场景会使用 List prefix 的方式按字典序获取目录下的所有文件(包括子目录),而这个场景在层级 namespace 下并不容易实现,需要通过并行 BFS 等方式搜索才能得到合适的结果。但是如果这类场景不做兼容的话,用户将当前基于平坦 namespace 对象存储开发的业务迁移到层级 namespace 后,则一定会出现一些不符合预期的结果从而影响体验。考虑到这些,层级 namespace 在支持常见的目录操作的同时, 为了兼容对象语义也实现了诸多特性:- 自动创建父目录(可以直接按照对象全名写入而不需逐个创建父目录);
- 按前缀 list(按照对象全名的字典序返回 List 结果);
2.7. HCFS 客户端优化
针对大数据计算场景通常使用 HCFS 系统来处理数据,我们在对象存储 BOS HDFS 中实现了对用户业务透明的加速优化,包括:计算场景下经常会有连续读一个大数据文件的场景,针对于这个需求,我们将客户的读请求拆分成多个可以并发读取操作。但是由于用户的读请求并非每次都需要获取整个对象,因此一旦太过激进的预读逻辑则会导致大量的带宽及后端 I/O 资源的浪费。因此只有当业务每次连续读的 offset 满足一定条件时,我们才会逐步放大向后端 read ahead 的 buf size 从而在避免读放大过大的条件下取得更好的读性能。
计算场景下往往采用 Parquet,ORC 等列存格式,这些文件格式往往有个特点是元数据信息位于文件特定的某些部分(例如末尾等)。当客户端发现用户在读取这类文件的时候,会在 read ahead 的逻辑中主动把末尾的一部分元信息缓存,这样可以减少未来可能的多次对文件元数据段的访问,从而提升整体性能。对象存储 BOS 的 HDFS 客户端可以智能识别用户的 bucket 桶的存储类型,针对层级 namespace 和平坦 namespace 使用不同的最为合适的访问方式,从而保证在计算过程中对用户业务的透明无感。2.8. 效果对比
测试 1:API 性能对比
针对对象存储 BOS 常见的 API,我们对比了 Raname(单文件),LS(目录下 100 文件),Put(100K),Head,Get(4M),Del 操作的性能数据。可以看到层级 namespace 在数据读上可以与平坦 namespace 持平。而在涉及到目录操作,对象元数据读取(HEAD,LIST,Del)等操作上,可以做到大幅领先平坦 namespace 的对象存储。
测试 2:NNBench 对比
针对 HDFS 的 NN Bench 测试,我们基于 30 Map,10 Reduce,对于小文件场景做了对比,可以看到基于层级 namespace 的性能数据相比平坦 namespace 有成倍的提升。
针对标准的 TPC-DS 10T 测试,我们可以看到层级 namepace 在总体性能上可以达到 10% 的提升。而在部分特别的 Query 语句中,相比平坦 namespace,层级 namepace 可以达到近 100% 的提升。层级 namespace 是为了数据分析场景专门优化的对象存储产品,元数据存储格式更有利于目录树语义的相关操作,对于 List Dir/Rename Dir(File)/Delete Dir 性能尤为突出。目录树的结构保留了文件与层次之间的相关性,但也因此带来了如下的问题:
- 路径解析网络延迟高,需要从 Root 依次递归读取相关节点元数据直到目标节点;
- 层级越小,访问热点越明显,从而导致底层存储负载严重不均衡。
而这些缺点恰恰是使用平坦 namespace 对象存储所擅长的优势:- 路径解析非常高效,直接根据请求提供的全路径做计算即可得到元数据;
- 无真正意义上目录概念,因此也不会存在“目录热点”,所有请求都可以被均匀打散,集群很容易做到线性扩容。
综上所述,BOS 层级 namespace 虽然在大数据计算场景有其独特的优势,但是平坦 namespace 对象存储在处理海量无依赖关系的数据场景,则具备更大的读写并发能力。我们也将在会支持两个对象存储产品之间的数据流转,方便用户根据业务数据的当前场景,随时做相应的调整和优化。此外,虽然基于对象存储底座的数据湖由于其存算分离的架构有着其巨大的优势,但是在大数据场景中由于庞大的分析数据流,导致网络传输带来的延迟也是一个不可忽视的缺点。数据的就近访问能够带来更显著的提升,百度沧海·存储的数据湖加速 RapidFS 能够很好地帮助解决这个问题,由于篇幅有限,后续的数据湖系列内容会有单独的篇章来详细介绍。3. 使用方法
由于层级 namespace 接口兼容已有的平坦 namespace 对象存储的绝大部分 API,因此原先基于平坦 namespace 的业务代码和逻辑几乎不需要变动就可以直接使用层级 namespace。同时我们也提供了兼容 HCFS 接口的 BOS HDFS jar 包,该包兼容了传统平坦 namespace 的操作,并且能够智能根据用户 bucket 的实际类型来选择最合适的操作去处理,以加速计算性能的目的。- - - - - - - - - - END - - - - - - - - - - 数据湖系列之一 | 你一定爱读的极简数据平台史,从数据仓库、数据湖到湖仓一体
数据湖系列之二 | 打造无限扩展的云存储系统,元数据存储底座的设计和实践
AI 应用的全流程存储加速方案技术解析和实践分享
面向高性能计算场景的存储系统解决方案
面向大数据存算分离场景的数据湖加速方案
万亿级对象存储的元数据系统架构设计和实践
AI 训练加速原理解析与工程实践分享
AI 推理加速原理解析与工程实践分享
大规模 AI 高性能网络的设计与实践